package eu.fbk.knowledgestore.internal.jaxrs;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.Nullable;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.CountingInputStream;
import com.google.common.io.CountingOutputStream;
import com.google.common.reflect.TypeToken;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.vocabulary.DCTERMS;
import org.openrdf.query.BindingSet;
import org.openrdf.query.resultio.BooleanQueryResultFormat;
import org.openrdf.query.resultio.TupleQueryResultFormat;
import org.openrdf.rio.RDFFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import eu.fbk.knowledgestore.Outcome;
import eu.fbk.knowledgestore.data.Data;
import eu.fbk.knowledgestore.data.Record;
import eu.fbk.knowledgestore.data.Representation;
import eu.fbk.knowledgestore.data.Stream;
import eu.fbk.knowledgestore.internal.Logging;
import eu.fbk.knowledgestore.internal.Util;
import eu.fbk.knowledgestore.internal.rdf.RDFUtil;
import eu.fbk.knowledgestore.vocabulary.KS;
import eu.fbk.knowledgestore.vocabulary.NFO;
import eu.fbk.knowledgestore.vocabulary.NIE;
import eu.fbk.rdfpro.tql.TQL;
@Provider
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.WILDCARD)
public class Serializer implements MessageBodyReader<Object>, MessageBodyWriter<Object> {
// TODO: supported types depend on imported libraries
private static final Logger LOGGER = LoggerFactory.getLogger(Serializer.class);
@Override
public boolean isReadable(final Class<?> type, final Type genericType,
final Annotation[] annotations, final MediaType mediaType) {
final boolean result = type.isAssignableFrom(Representation.class)
|| isAssignable(genericType, Protocol.STREAM_OF_RECORDS.getType())
|| isAssignable(genericType, Protocol.STREAM_OF_OUTCOMES.getType())
|| isAssignable(genericType, Protocol.STREAM_OF_STATEMENTS.getType())
|| isAssignable(genericType, Protocol.STREAM_OF_TUPLES.getType())
|| isAssignable(genericType, Protocol.STREAM_OF_BOOLEANS.getType());
if (!result) {
LOGGER.debug("Non deserializable stream: {} ({})", genericType, mediaType);
}
return result;
}
@Override
public boolean isWriteable(final Class<?> type, final Type genericType,
final Annotation[] annotations, final MediaType mediaType) {
final boolean result = Representation.class.isAssignableFrom(type)
|| isAssignable(Protocol.STREAM_OF_RECORDS.getType(), genericType)
|| isAssignable(Protocol.STREAM_OF_OUTCOMES.getType(), genericType)
|| isAssignable(Protocol.STREAM_OF_STATEMENTS.getType(), genericType)
|| isAssignable(Protocol.STREAM_OF_TUPLES.getType(), genericType)
|| isAssignable(Protocol.STREAM_OF_BOOLEANS.getType(), genericType);
if (!result) {
LOGGER.debug("Non serializable stream: {} ({})", genericType, mediaType);
}
return result;
}
@Override
public long getSize(final Object object, final Class<?> type, final Type genericType,
final Annotation[] annotations, final MediaType mediaType) {
throw new UnsupportedOperationException(); // JAX-RS promises never to call this method
}
@SuppressWarnings("resource")
@Override
public Object readFrom(final Class<Object> type, final Type genericType,
final Annotation[] annotations, final MediaType mediaType,
final MultivaluedMap<String, String> headers, final InputStream input)
throws IOException, WebApplicationException {
final String mimeType = mediaType.getType() + "/" + mediaType.getSubtype();
final CountingInputStream in = new CountingInputStream(input);
final boolean chunked = "true".equalsIgnoreCase(headers.getFirst(Protocol.HEADER_CHUNKED));
final long ts = System.currentTimeMillis();
try {
if (type.isAssignableFrom(Representation.class)) {
final InputStream stream = interceptClose(in, ts);
final Representation representation = Representation.create(stream);
readMetadata(representation.getMetadata(), headers);
return representation;
} else if (isAssignable(genericType, Protocol.STREAM_OF_RECORDS.getType())) {
final RDFFormat format = formatFor(mimeType);
final AtomicLong numStatements = new AtomicLong();
final AtomicLong numRecords = new AtomicLong();
Stream<Statement> statements = RDFUtil.readRDF(in, format, null, null, false);
statements = statements.track(numStatements, null);
Stream<Record> records = Record.decode(statements, null, chunked);
records = records.track(numRecords, null);
interceptClose(records, in, ts, numRecords, "record(s)", numStatements,
"statement(s)");
return records;
} else if (isAssignable(genericType, Protocol.STREAM_OF_OUTCOMES.getType())) {
final RDFFormat format = formatFor(mimeType);
final AtomicLong numStatements = new AtomicLong();
final AtomicLong numOutcomes = new AtomicLong();
Stream<Statement> statements = RDFUtil.readRDF(in, format, null, null, false);
statements = statements.track(numStatements, null);
Stream<Outcome> outcomes = Outcome.decode(statements, chunked);
outcomes = outcomes.track(numOutcomes, null);
interceptClose(outcomes, in, ts, numOutcomes, "outcome(s)", numStatements,
"statement(s)");
return outcomes;
} else if (isAssignable(genericType, Protocol.STREAM_OF_STATEMENTS.getType())) {
final RDFFormat format = formatFor(mimeType);
final AtomicLong numStatements = new AtomicLong();
Stream<Statement> statements = RDFUtil.readRDF(in, format, null, null, false);
statements = statements.track(numStatements, null);
interceptClose(statements, in, ts, numStatements, "statement(s)");
return statements;
} else if (isAssignable(genericType, Protocol.STREAM_OF_TUPLES.getType())) {
final TupleQueryResultFormat format;
format = TupleQueryResultFormat.forMIMEType(mimeType);
final AtomicLong numTuples = new AtomicLong();
Stream<BindingSet> tuples = RDFUtil.readSparqlTuples(format, in);
tuples = tuples.track(numTuples, null);
interceptClose(tuples, in, ts, numTuples, "tuple(s)");
return tuples;
} else if (isAssignable(genericType, Protocol.STREAM_OF_BOOLEANS.getType())) {
final BooleanQueryResultFormat format;
format = BooleanQueryResultFormat.forMIMEType(mimeType);
final boolean result = RDFUtil.readSparqlBoolean(format, in);
final Stream<Boolean> stream = Stream.create(result);
interceptClose(stream, in, ts, 1, "boolean");
return stream;
}
} catch (final Throwable ex) {
Util.closeQuietly(in); // done even if advised against it
Throwables.propagateIfPossible(ex, IOException.class);
throw Throwables.propagate(ex);
}
throw new IllegalArgumentException("Cannot deserialize " + genericType + " from "
+ mimeType);
}
@SuppressWarnings("unchecked")
@Override
public void writeTo(final Object object, final Class<?> type, final Type genericType,
final Annotation[] annotations, final MediaType mediaType,
final MultivaluedMap<String, Object> headers, final OutputStream output)
throws IOException, WebApplicationException {
final String mimeType = mediaType.getType() + "/" + mediaType.getSubtype();
final Map<String, String> namespaces = Data.getNamespaceMap();
final CountingOutputStream out = new CountingOutputStream(output);
final long ts = System.currentTimeMillis();
try {
if (Representation.class.isAssignableFrom(type)) {
final Representation representation = (Representation) object;
writeMetadata(representation.getMetadata(), headers);
representation.writeTo(out);
logWrite(ts, out);
} else if (isAssignable(Protocol.STREAM_OF_RECORDS.getType(), genericType)) {
headers.putSingle(Protocol.HEADER_CHUNKED, "true");
final String mime = setupType(mimeType, Protocol.MIME_TYPES_RDF, headers);
final RDFFormat format = formatFor(mime);
final AtomicLong recordCounter = new AtomicLong();
Stream<? extends Record> records = (Stream<? extends Record>) object;
records = records.track(recordCounter, null);
final Stream<Statement> stmt = Record.encode(records, null);
final long count = RDFUtil.writeRDF(out, format, namespaces, null, stmt);
logWrite(ts, out, recordCounter.get(), "record(s)", count, "statement(s)");
} else if (isAssignable(Protocol.STREAM_OF_OUTCOMES.getType(), genericType)) {
headers.putSingle(Protocol.HEADER_CHUNKED, "true");
final String mime = setupType(mimeType, Protocol.MIME_TYPES_RDF, headers);
final RDFFormat format = formatFor(mime);
final AtomicLong outcomeCounter = new AtomicLong();
Stream<? extends Outcome> outcomes = (Stream<? extends Outcome>) object;
outcomes = outcomes.track(outcomeCounter, null);
final Stream<Statement> stmt = Outcome.encode(outcomes);
final long count = RDFUtil.writeRDF(out, format, namespaces, null, stmt);
logWrite(ts, out, outcomeCounter.get(), "outcome(s)", count, "statement(s)");
} else if (isAssignable(Protocol.STREAM_OF_STATEMENTS.getType(), genericType)) {
final String mime = setupType(mimeType, Protocol.MIME_TYPES_RDF, headers);
final RDFFormat format = formatFor(mime);
final Stream<? extends Statement> stmt = (Stream<? extends Statement>) object;
final long count = RDFUtil.writeRDF(out, format, namespaces, null, stmt);
logWrite(ts, out, count, "statement(s)");
} else if (isAssignable(Protocol.STREAM_OF_TUPLES.getType(), genericType)) {
final String mime = setupType(mimeType, Protocol.MIME_TYPES_SPARQL_TUPLE, headers);
final TupleQueryResultFormat format = TupleQueryResultFormat.forMIMEType(mime);
final Stream<? extends BindingSet> tuples = (Stream<? extends BindingSet>) object;
final long count = RDFUtil.writeSparqlTuples(format, out, tuples);
logWrite(ts, out, count, "tuple(s)");
} else if (isAssignable(Protocol.STREAM_OF_BOOLEANS.getType(), genericType)) {
final String mime = setupType(mimeType, Protocol.MIME_TYPES_SPARQL_BOOLEAN,
headers);
final BooleanQueryResultFormat format = BooleanQueryResultFormat.forMIMEType(mime);
final boolean bool = ((Stream<? extends Boolean>) object).getUnique();
RDFUtil.writeSparqlBoolean(format, out, bool);
logWrite(ts, out, 1, "boolean");
} else {
throw new IllegalArgumentException("Cannot serialize " + genericType + " to "
+ mediaType);
}
} finally {
Util.closeQuietly(object);
Util.closeQuietly(out); // done even if asked not to do so
}
}
@Nullable
private static void readMetadata(final Record metadata,
final MultivaluedMap<String, String> headers) {
// Read Content-Type header
final String mime = headers.getFirst(HttpHeaders.CONTENT_TYPE);
metadata.set(NIE.MIME_TYPE, mime != null ? mime : MediaType.APPLICATION_OCTET_STREAM);
// Read Content-MD5 header, if available
final String md5 = headers.getFirst("Content-MD5");
if (md5 != null) {
final Record hash = Record.create();
hash.set(NFO.HASH_ALGORITHM, "MD5");
hash.set(NFO.HASH_VALUE, md5);
metadata.set(NFO.HAS_HASH, hash);
}
// Read Content-Language header, if possible
final String language = headers.getFirst(HttpHeaders.CONTENT_LANGUAGE);
try {
metadata.set(DCTERMS.LANGUAGE, Data.languageCodeToURI(language));
} catch (final Throwable ex) {
LOGGER.warn("Invalid {}: {}", HttpHeaders.CONTENT_LANGUAGE, language);
}
// Read custom X-KS-Meta header
final String encodedMeta = headers.getFirst(Protocol.HEADER_META);
if (encodedMeta != null) {
final InputStream in = new ByteArrayInputStream(encodedMeta.getBytes(Charsets.UTF_8));
final Stream<Statement> statements = RDFUtil.readRDF(in, RDFFormat.TURTLE,
Data.getNamespaceMap(), null, true);
final Record record = Record.decode(statements,
ImmutableSet.<URI>of(KS.REPRESENTATION), true).getUnique();
metadata.setID(record.getID());
for (final URI property : record.getProperties()) {
metadata.set(property, record.get(property));
}
}
}
@Nullable
private static void writeMetadata(final Record metadata,
final MultivaluedMap<String, Object> headers) {
// Write Content-Type header
headers.putSingle(HttpHeaders.CONTENT_TYPE, metadata.getUnique(NIE.MIME_TYPE,
String.class, MediaType.APPLICATION_OCTET_STREAM));
// Write Content-MD5 header, if possible
final Record hash = metadata.getUnique(NFO.HAS_HASH, Record.class, null);
final String md5 = hash == null ? null : !"MD5".equals(hash.getUnique(NFO.HASH_ALGORITHM,
String.class, null)) ? null //
: hash.getUnique(NFO.HASH_VALUE, String.class, null);
headers.putSingle("Content-MD5", md5);
// Write Content-Language header, if possible
String language = metadata.getUnique(NIE.LANGUAGE, String.class, null);
if (language == null) {
final URI languageURI = metadata.getUnique(DCTERMS.LANGUAGE, URI.class, null);
try {
language = Data.languageURIToCode(languageURI);
} catch (final Throwable ex) {
LOGGER.warn("Invalid language URI: ", languageURI);
}
}
headers.putSingle(HttpHeaders.CONTENT_LANGUAGE, language);
// Write custom X-KS-Meta header
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final Stream<Statement> statements = Record.encode(Stream.create(metadata),
ImmutableSet.<URI>of(KS.REPRESENTATION));
RDFUtil.writeRDF(out, RDFFormat.TURTLE, Data.getNamespaceMap(), null, statements);
final String string = new String(out.toByteArray(), Charsets.UTF_8);
final StringBuilder builder = new StringBuilder();
String separator = "";
for (final String line : Splitter.on('\n').trimResults().omitEmptyStrings().split(string)) {
if (!line.toLowerCase().startsWith("@prefix")) {
builder.append(separator).append(line);
separator = " ";
}
}
headers.putSingle(Protocol.HEADER_META, builder.toString());
}
private static boolean isAssignable(final Type lhs, final Type rhs) {
return TypeToken.of(lhs).isAssignableFrom(rhs);
}
private static String setupType(final String jaxrsType, final String supportedTypes,
final MultivaluedMap<String, Object> headers) {
if (jaxrsType != null) {
return jaxrsType;
}
final int index = supportedTypes.indexOf(',');
final String mediaType = index < 0 ? supportedTypes : supportedTypes.substring(0, index);
headers.putSingle("Content-Type", mediaType);
headers.remove("ETag"); // to stay on the safe side
return mediaType;
}
private static void interceptClose(final Stream<?> stream, final CountingInputStream in,
final long startTime, final Object... args) {
final Map<String, String> mdc = Logging.getMDC();
stream.onClose(new Runnable() {
@Override
public void run() {
final Map<String, String> oldMdc = Logging.getMDC();
try {
Logging.setMDC(mdc);
logRead(in, startTime, args);
// closing the stream should not be done, but GZIPFilter seems not to detect
// EOF and does not release the underlying stream, causing the connection not
// to be released
Util.closeQuietly(in);
} finally {
Logging.setMDC(oldMdc);
}
}
});
}
private static InputStream interceptClose(final CountingInputStream stream,
final long startTime, final Object... args) {
final Map<String, String> mdc = Logging.getMDC();
return new FilterInputStream(stream) {
private boolean closed;
@Override
public void close() throws IOException {
if (this.closed) {
return;
}
final Map<String, String> oldMdc = Logging.getMDC();
try {
Logging.setMDC(mdc);
logRead(stream, startTime, args);
// closing the stream should not be done, but GZIPFilter seems not to detect
// EOF and does not release the underlying stream, causing the connection not
// to be released
Util.closeQuietly(this.in);
} finally {
this.closed = true;
Logging.setMDC(oldMdc);
super.close();
}
}
};
}
private static void logRead(final CountingInputStream in, final long startTime,
final Object... args) {
if (LOGGER.isDebugEnabled()) {
boolean eof = false;
try {
eof = in.read() == -1;
} catch (final Throwable ex) {
// ignore
}
final long elapsed = System.currentTimeMillis() - startTime;
final StringBuilder builder = new StringBuilder();
builder.append("Http: read complete, ");
for (int i = 0; i < args.length; i += 2) {
builder.append(args[i]).append(" ").append(args[i + 1]).append(", ");
}
builder.append(in.getCount()).append(" byte(s), ");
if (eof) {
builder.append("EOF, ");
}
builder.append(elapsed).append(" ms");
LOGGER.debug(builder.toString());
}
}
private static void logWrite(final long startTime, final CountingOutputStream stream,
final Object... args) {
if (LOGGER.isDebugEnabled()) {
final long elapsed = System.currentTimeMillis() - startTime;
final StringBuilder builder = new StringBuilder();
builder.append("Http: write complete, ");
for (int i = 0; i < args.length; i += 2) {
builder.append(args[i]).append(" ").append(args[i + 1]).append(", ");
}
builder.append(stream.getCount()).append(" byte(s), ");
builder.append(elapsed).append(" ms");
LOGGER.debug(builder.toString());
}
}
private static RDFFormat formatFor(final String mimeType) {
final RDFFormat format = RDFFormat.forMIMEType(mimeType);
if (format == null) {
throw new IllegalArgumentException("No RDF format for MIME type '" + mimeType + "'");
}
return format;
}
static {
RDFFormat.register(TQL.FORMAT);
}
}